import StreamSlicer from 'stream-slicer';
import through2 from 'through2'
import debug2 from 'debug'
import errors from './errors.js'
import helper from './lib/helper.js'
import execFile2 from './lib/execFile.js'
import cscript from './lib/cscript.js'
let execFile = execFile2()

export class Regedit {
  fs =  window.require('fs')
  util = window.require('util')
  childProcess = window.require('child_process')
  path = window.require('path')
  os = window.require('os')
  debug = debug2('regedit')

  OS_ARCH_AGNOSTIC = 'A';

  /*
  * 	Access the registry using a specific os architecture, but determine what the architecture is automatically
  * 	This means that accessing in order to access the 32bit software registry on a 64bit machine we will need to
  * 	use the key hklm\software\wow6432node
  */
  OS_ARCH_SPECIFIC = 'S';

  /*
  * 	Access the registry using 32bit os architecture
  */
  OS_ARCH_32BIT = '32';

  /*
  * 	Access the registry using 64bit os architecture, this will have no effect on 32bit process/machines
  */
  OS_ARCH_64BIT = '64';
  externalVBSFolderLocation=''


  setExternalVBSLocation(newLocation:string){
    if (this.fs.existsSync(newLocation)) {
      this.externalVBSFolderLocation = newLocation;
      return 'Folder found and set';
    }
  
    return 'Folder not found';

  }
  list(keys, architecture, callback?:Function) {
    //// console.log('list with callback will be deprecated in future versions, use list streaming interface')
  
    if (architecture === undefined) {
      callback = undefined;
      architecture = this.OS_ARCH_AGNOSTIC;
    } else if (typeof architecture === 'function') {
      callback = architecture;
      architecture = this.OS_ARCH_AGNOSTIC;
    }
  
    if (typeof keys === 'string') {
      keys = [keys];
    }
  
    if (typeof callback === 'function') {
      this.execute(this.toCommandArgs('regList.wsf', architecture, keys), callback);
    } else {
      let outputStream = through2.obj(helper.vbsOutputTransform);
  
      cscript.init((err)=> {
        if (err) {
          return outputStream.emit('error', err);
        }
  
        let args = this.baseCommand('regListStream.wsf', architecture);
  
        let child = execFile(cscript.path(), args, { encoding: 'utf8' }, (err)=> {
          if (err) {
            outputStream.emit('error', err);
          }
        });
  
        child.stderr.pipe(process.stderr);
  
        let slicer = new StreamSlicer({ sliceBy: helper.WIN_EOL });
  
        child.stdout.pipe(slicer).pipe(outputStream);
  
        helper.writeArrayToStream(keys, child.stdin);
      });
  
      return outputStream;
    }
  }
  createKey(keys, architecture?, callback?) {
    if (typeof architecture === 'function') {
      callback = architecture;
      architecture = this.OS_ARCH_AGNOSTIC;
    }
  
    if (typeof keys === 'string') {
      keys = [keys];
    }
  
    let args = this.baseCommand('regCreateKey.wsf', architecture);
  
    this.spawnEx(args, keys, callback);
  }
  deleteKey(keys, architecture?, callback?) {
    if (typeof architecture === 'function') {
      callback = architecture;
      architecture = this.OS_ARCH_AGNOSTIC;
    }
  
    if (typeof keys === 'string') {
      keys = [keys];
    }
  
    let args = this.baseCommand('regDeleteKey.wsf', architecture);
  
    this.spawnEx(args, keys, callback);
  }
  putValue(map, architecture?, callback?) {
    if (typeof architecture === 'function') {
      callback = architecture;
      architecture = this.OS_ARCH_AGNOSTIC;
    }
  
    let args = this.baseCommand('regPutValue.wsf', architecture);
  
    let values = [];
  
    for (let key in map) {
      let keyValues = map[key];
  
      for (let valueName in keyValues) {
        let entry = keyValues[valueName];
  
        //// helper writes the array to the stream in reversed order
        values.push(entry.type);
        values.push(this.renderValueByType(entry.value, entry.type));
        values.push(valueName);
        values.push(key);
      }
    }
  
    this.spawnEx(args, values, callback);
  }
  arch = {
    list: (keys, callback) =>{
      return this.list(keys, this.OS_ARCH_SPECIFIC, callback);
    },
    list32: (keys, callback)=> {
      return this.list(keys, this.OS_ARCH_32BIT, callback);
    },
    list64 : (keys, callback)=> {
      return this.list(keys,this.OS_ARCH_64BIT, callback);
    },
    createKey : (keys, callback)=> {
      return this.createKey(keys, this.OS_ARCH_SPECIFIC, callback)
    },
    
   createKey32 : (keys, callback)=> {
      return this.createKey(keys, this.OS_ARCH_32BIT, callback);
    },
    
   createKey64 : (keys, callback)=> {
      return this.createKey(keys, this.OS_ARCH_64BIT, callback)
    },
    
   deleteKey : (keys, callback)=> {
      return this.deleteKey(keys, this.OS_ARCH_SPECIFIC, callback)
    },
    
   deleteKey32 : (keys, callback)=> {
      return this.deleteKey(keys, this.OS_ARCH_32BIT, callback)
    },
    
   deleteKey64 : (keys, callback)=> {
      return this.deleteKey(keys, this.OS_ARCH_64BIT, callback)
    },
    
   putValue : (keys, callback)=> {
      return this.putValue(keys, this.OS_ARCH_SPECIFIC, callback)
    },
    
   putValue32 : (keys, callback)=> {
      return this.putValue(keys, this.OS_ARCH_32BIT, callback)
    },
    
   putValue64 : (keys, callback)=> {
      return this.putValue(keys, this.OS_ARCH_64BIT, callback)
    }

  }
  execute(args, callback) {

    if (typeof callback !== 'function') {
      throw new Error('missing callback');
    }
  
    this.debug(args);
  
    cscript.init((err)=> {
      if (err) {
        return callback(err);
      }
  
      this.childProcess.execFile(cscript.path(), args, (err, stdout, stderr)=> {
  
        if (err) {
          if (stdout) {
            console.log(stdout);
          }
  
          if (stderr) {
            console.error(stderr);
          }
  
          if (err.code in errors) {
            return callback(errors[err.code]);
          }
          return callback(err);
  
        }
  
        //// in case we have stuff in stderr but no real error
        if (stderr) {
          return callback(new Error(stderr));
        }
        if (!stdout) {
          return callback();
        }
  
        this.debug(stdout);
  
        let result;
        err = null;
  
        try {
          result = JSON.parse(stdout);
        } catch (e) {
          e.stdout = stdout;
          err = e;
        }
  
        callback(err, result);
      });
    });
  }
  spawnEx(args, keys, callback) {
    cscript.init((err)=> {
      if (err) {
        return callback(err);
      }
  
      this.debug(args);
  
      let child = execFile(cscript.path(), args, { encoding: 'utf8' });
  
      this.handleErrorsAndClose(child, callback);
  
      helper.writeArrayToStream(keys, child.stdin);
    });
  }
  handleErrorsAndClose(child, callback) {
    let error;
    child.once('error', (e)=> {
      this.debug('process error %s', e);
      error = e;
    });
  
    child.once('close', (code)=> {
      this.debug('process exit with code %d', code);
  
      if (error) {
        if (error.code in errors) {
          return callback(errors[error.code]);
        }
        return callback(error);
  
      }
  
      if (code !== 0) {
        if (code in errors) {
          return callback(errors[code]);
        }
        return callback(new Error('vbscript process reported unknown error code ' + code));
  
      }
  
      callback();
    });
  }
  renderValueByType(value, type) {
    type = type.toUpperCase();
  
    switch (type) {
      case 'REG_BINARY':
        if (!this.util.isArray(value)) {
          throw new Error('invalid value type ' + typeof(value) + ' for registry type REG_BINARY, please use an array of numbers');
        }
        return value.join(',');
  
      case 'REG_MULTI_SZ':
        if (!this.util.isArray(value)) {
          throw new Error('invalid value type ' + typeof(value) + ' for registry type REG_BINARY, please use an array of strings');
        }
        return value.join(',');
  
      case 'REG_SZ':
        if (value === '') {
          return '\0';
        }
        return value;
  
      default:
        return value;
    }
  }
  //// TODO: move to helper.js?
    toCommandArgs(cmd, arch, keys) {
      let result = this.baseCommand(cmd, arch);
      if (typeof keys === 'string') {
        result.push(keys);
      } else if (this.util.isArray(keys)) {
        result = result.concat(keys);
      } else {
        this.debug('creating command without using keys %s', keys ? keys : '');
      }

      return result;
    }
    baseCommand(cmd, arch) {

      let scriptPath;
      let appPath = window.process.cwd();
      let defaultFilePath = window.process.env.NODE_ENV === 'development' ? window.$ele.path.resolve(appPath, './src/util/regedit/vbs') : window.$ele.path.resolve(appPath, './resources/regedit/vbs');
      //// test undefined, null and empty string
      if (this.externalVBSFolderLocation && typeof(this.externalVBSFolderLocation) === 'string') {
        scriptPath = this.externalVBSFolderLocation;
      } else {
        scriptPath = defaultFilePath;
      }
    
      return ['////Nologo', this.path.join(scriptPath, cmd), arch];
    }
}
